基本语法
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值
call()
和 apply()
返回函数应该返回的值,bind()
返回一个经过硬绑定的新函数。
参数介绍
第一个参数为 thisArg,其取值有以下几种情况:
不传/ 传null/ 传undefined:非严格模式下,this 指向 window 对象;严格模式下指向 undefined;
传递基本类型:this 指向其对应的包装对象,如 String、Number、Boolean
传递一个对象:函数中的 this 指向这个对象
第二个参数有以下几种情况:
- 不传/ 传null/ 传undefined:表示不需要传入任何参数
call()
和bind()
的第二个参数都是参数列表,而apply()
则是参数数组(或者类数组)—— 尽管如此,在这些参数传递给调用函数时,仍然是以参数列表的形式传递的(这一点很重要)。
执行
call()
和 apply()
一经调用则立即执行函数,而 bind()
则只是完成了函数的 this 绑定。因为函数不会立刻执行,所以适合在事件绑定函数中使用 bind()
,这样既完成了绑定,也确保了仅当事件触发时才执行函数。
应用场景
在这篇文章说过,call()
,apply()
和 bind()
都可以改变 this 的指向,什么时候需要改变 this 的指向呢?大部分时候其实是为了借用方法,即在对象上调用其自身不具备的方法。看一下下面的例子:
1. 方法借用:判断数据类型
利用 Object.prototype.toString.call()
可以准确地判断数据类型,如:
var a = "abc";
var b = [1,2,3];
Object.prototype.toString.call(a) == "[object String]" //true
Object.prototype.toString.call(b) == "[object Array]" //true
原理就是:在任何值上调用 Object 原生的 toString() 方法,都会返回一个格式为 [object NativeconstructorName] 的字符串。据此可以准确判断任何值的数据类型。
既然 Array 和 Function 都继承了 Object 的该方法,为什么不直接在它们身上调用?这是因为 toString()
被重写过了,不是原生方法,因此这里改为调用 Object 的该方法,并将 this 绑定给对应的值。
2. 方法借用:类数组使用数组方法
例如 arguments 是类数组,并不具备数组的 forEach()
方法,那么我们可以通过 call()
调用数组的该方法,同时将方法里面的 this 绑定到 arguments 上:
Array.prototype.forEach.call(arguments,function(item){
console.log(item);
});
类数组借用数组的方法,还可以将类数组转化为数组:
Array.prototype.slice.call(arguments) // slice 本身会返回一个数组
当然还有其它转化方法:
[].slice.call(arguments)
[...arguments]
Array.from(arguments)
3. 模拟浅拷贝
模拟浅拷贝的过程中,需要剔除原型链上的属性,考虑到源对象可能基于 Object.create()
创建,而这样的对象是没有 hasOwnProperty()
方法的,因此我们不在源对象身上直接调用该方法,而是通过 Object.prototype.hasOwnProperty.call()
的方式去调用,因为 Object 一定是有这个方法的,我们可以借用一下。
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
4. 增强子类实例
JavaScript 的几种继承方式中,有一种就是借用构造函数:
假设有子构造函数 Son
和父构造函数 Parent
。对于 Son
而言,其内部的 this 将指向稍后实例化的对象,利用这一点,我们在 Son
的内部通过 call()
或者 apply()
调用 Parent
,同时传参 this,这样就可以增强子类实例。
5. 求数组的最值
apply()
可用于展开数组,即传进去的第二个参数是一个参数数组,但实际执行的时候会被转化为一个参数列表。利用这一点,我们可以求一个数组的最大值 —— 虽然 Math 对象有 max()
方法,但该方法只接受参数列表。那么这时候,我们可以将该方法以 apply()
的方式去调用,从而展开数组:
var arr = [2,3,1,5,4];
Math.max.apply(null,arr);// 5
6. 延迟执行函数的 this 绑定
bind
和 call
/ apply
的一个重要区别就在于,它只是对原函数提前做了一个 this 绑定,并没有马上去执行函数。因此对于那些延迟执行但又容易发生 this 丢失的函数(比如定时器的回调函数),我们可以在声明的时候先通过 bind 绑定一个 this。比如:
var value = 1
const obj = {
value : 2,
fn(){
setTimeout(function(){
console.log(this.value)
},1000)
}
}
obj.fn() // 1
像这样直接调用会发生 this 丢失的问题,因为 setTimeout 本质上还是通过 window 调用的,所以 this 会指向 window。而这个回调函数我们又不想马上执行,只是想它在执行的时候绑定一个正确的 this,因此我们可以使用 bind:
var value = 1
const obj = {
value : 2,
fn(){
setTimeout(function(){
console.log(this.value)
}.bind(this),1000)
}
}
obj.fn() // 2
7. 实现柯里化包装函数
比如说现在有一个 add 函数:
function add(a,b){
return a + b
}
add(1,2) // 3
如果想要让 add 函数做到类似 add(1)(2)
这样的分批次接收参数,且最终执行结果是一样的,应该怎么办呢?可以将 add 改写如下:
function add(a){
return function(b){
return a + b
}
}
但是参数是灵活的,我们更希望实现一个通用的柯里化包装函数,传进去的函数经过包装之后,会返回一个原函数的柯里化版本。这里就可以使用 bind 来实现了:
function curry(fn){
const len = fn.length
return function fnCurried(){
if(arguments.length < len){
return fnCurried.bind(null,...arguments)
} else {
return fn(...arguments)
}
}
}
const curryAdd = curry(add)
curryAdd(1,2) // 3
curryAdd(1)(2) // 3
几个要点:
- 将目标函数 fn 传给包装函数 curry 之后,会返回一个柯里化版本的函数 fnCurried。对于 fnCurried,我们在调用的时候,可以选择一次性传完所有参数,也可以选择分批次传参数,那么如何判断呢?如果是分批次传参的话,传的参数个数
arguments.length
一定会小于 fn 实际应该接受的参数fn.length
,反之则是一次性传所有参数 - 如果是一次性传所有参数,那就比较简单了,直接返回原函数 fn 的调用结果就行(注意要展开 arguments)
- 如果是分批次传参数,那么就重复返回相同的函数以供下次进行同样的调用。这里我们使用 bind 并不是为了修改 this 指向,所以传一个 null 就行,我们只是为了利用 bind 可以分批次传参这个特点来收集每次调用得到的参数而已。
参考:
https://www.cnblogs.com/onepixel/p/6034307.html
https://juejin.im/post/5d469e0851882544b85c32ef